library(dplyr)
library(readr)
library(mice)
library(stringr)
library(ggplot2)
library(tidyverse)
library(gridExtra)
library(gmodels)
library(Hmisc)
library(ggthemes)

library(ellipse)
library(corrplot)
library(ggcorrplot)
library(corrplot)
library(leaps)
library(PerformanceAnalytics)
library(GGally)
library(psych)
library(carData)
library(car)
library(lmtest)
library(olsrr)
library(performance)
library(see)
library(lme4)
library(patchwork)

Introducción

En esta sección se comenzarán a aplicar los algoritmos de aprendizaje automático (o machine learning) vistos en clase. Utilizaremos los siguientes algoritmos que podemos clasificar en:

  • Algoritmos no supervisados: Parten de un conjunto de datos sin etiquetar y aplican inducción partiendo de ejemplos para predecir en nuevos datos. En nuestro caso no utilizarán la etiqueta de si los teoremas son resolubles o no.

    • Análisis de componentes principales (reducción de la dimensionalidad)
    • k-medias. Análisis cluster no jerárquico o de conglomerados (clustering)
    • Análisis cluster jerárquico o de conglomerados (clustering)
  • Algoritmos supervisados: Utilizan un conjunto de datos etiquetados y con ellas intentan encontrar relaciones entre las características y las etiquetas para predecir en nuevos datos. En nuestro caso utilizarán la etiqueta de si los teoremas son resolubles o no.

    • Regresión logística
    • Los k-vecinos más cercanos (k-NN: The k-nearest neighbours) (Basado en la información)
    • Árboles de decisión (Basado en la información)
    • Bosques aleatorios (Basado en la información)
    • Máquinas de vectores de soporte (SVM: Support Vector Machines) (Basado en el error)

Se carga, limpia, visualiza y secciona el conjunto de datos:

data <- read.csv("train-ML.csv", na = c("","NA","NULL",NULL,"  ","/n" ))
head(data)
dataTest <- read.csv("test-ML.csv", na = c("","NA","NULL",NULL,"  ","/n" ))
head(dataTest)
library(dplyr)
data %>% select(-Price.mod2) -> data
data %>% select(-X) -> data
head(data)
data %>%
  mutate(Fuel_Type = as.factor(Fuel_Type)) %>%
  mutate(Transmission = as.factor(Transmission)) %>%
  mutate(Location = as.factor(Location)) -> data
data %>% mutate(Owner_Type = factor(Owner_Type, levels=c("First", "Second", "Third", "Fourth & Above"))) -> data

dataTest %>%
  mutate(Fuel_Type = as.factor(Fuel_Type)) %>%
  mutate(Transmission = as.factor(Transmission)) %>%
  mutate(Location = as.factor(Location)) -> dataTest

dataTest %>%
  mutate(Name = as.factor(Name)) %>%
  mutate(Make = as.factor(Make)) %>%
  mutate(Seats = as.factor(Seats))%>%
  mutate(Gama = as.factor(Gama))-> dataTest

data %>%
  mutate(Name = as.factor(Name)) %>%
  mutate(Make = as.factor(Make)) %>%
  mutate(Seats = as.factor(Seats))-> data

dataTest %>% mutate(Owner_Type = factor(Owner_Type, levels=c("First", "Second", "Third", "Fourth & Above"))) -> dataTest

Análisis no supervisado

En primer lugar, comenzaremos viendo los algoritmos de aprendizaje no supervisado.

Análisis de componentes principales

El análisis de componentes principales (en inglés, PCA) es una técnica utilizada para describir un conjunto de datos en términos de nuevas variables denominadas componentes no correlacionadas. Estas nuevas componentes se construyen a partir de las variables existentes, eso sí, debemos asegurarnos de que las variables utilizadas en PCA sean variables cuantitativas (no podemos usar variables cualitativas ni categóricas). Con esta técnica se pretende reducir la dimensionalidad del problema en cuestión.

library(corrplot)
str(data)
## 'data.frame':    4711 obs. of  15 variables:
##  $ Name             : Factor w/ 1636 levels "Ambassador Classic Nova Diesel",..: 37 72 501 1103 443 221 1073 1248 290 121 ...
##  $ Location         : Factor w/ 11 levels "Ahmedabad","Bangalore",..: 10 10 3 8 11 5 10 4 10 3 ...
##  $ Year             : int  2015 2014 2016 2009 2005 2016 2016 2017 2011 2012 ...
##  $ Kilometers_Driven: int  48000 18600 18000 80464 123200 46727 17000 30090 32000 115000 ...
##  $ Fuel_Type        : Factor w/ 4 levels "CNG","Diesel",..: 2 2 4 2 4 2 4 2 4 2 ...
##  $ Transmission     : Factor w/ 2 levels "Automatic","Manual": 1 1 1 1 2 2 1 1 2 1 ...
##  $ Owner_Type       : Factor w/ 4 levels "First","Second",..: 1 2 1 2 2 1 1 1 2 3 ...
##  $ Engine           : int  1968 1995 1197 2987 1495 1498 1595 1461 1196 1995 ...
##  $ Power            : num  174 190 82 198 94 ...
##  $ Seats            : Factor w/ 8 levels "2","4","5","6",..: 3 3 3 3 3 3 3 3 3 3 ...
##  $ Price            : num  18.25 21 5.4 9.29 1 ...
##  $ Make             : Factor w/ 28 levels "Ambassador","Audi",..: 2 4 11 18 11 9 18 23 9 4 ...
##  $ Gama             : chr  "Gama alta" "Gama alta" "Gama media" "Gama alta" ...
##  $ kmpl             : num  15.7 22.7 18.9 11 13.2 ...
##  $ kmpkg            : num  0 0 0 0 0 0 0 0 0 0 ...
data
corrplot(cor(data[, c(3,4, 8, 9, 14, 15)]), method = "ellipse") 

corPlot(data[, c(3,4, 8, 9, 14, 15)], cex = 1.2, main = "Matriz de correlación")

corrplot(cor(data[, c(3,4, 8, 9, 14, 15)]),method = "circle",       order = "hclust",         hclust.method = "ward.D",
         addrect =2,rect.col = 3,rect.lwd = 3)  

cortest(cor(data[, c(3,4, 8, 9, 14, 15)]))
## Tests of correlation matrices 
## Call:cortest(R1 = cor(data[, c(3, 4, 8, 9, 14, 15)]))
##  Chi Square value 250.68  with df =  15   with probability < 9e-45

Tenemos evidencias para decir que las correlaciones son distintas de 0

pca1 <- prcomp(data[, c(3,4, 8, 9, 14, 15)])
plot(pca1)

summary(pca1)
## Importance of components:
##                           PC1       PC2   PC3   PC4   PC5   PC6
## Standard deviation     101058 600.56073 27.03 4.409 3.046 1.968
## Proportion of Variance      1   0.00004  0.00 0.000 0.000 0.000
## Cumulative Proportion       1   1.00000  1.00 1.000 1.000 1.000

En nuestro conjunto de datos inicial, todas nuestras variables eran cuantitativas (menos la variable respuesta, que no utilizamos en aprendizaje no supervisado), sin embargo, las variables categorizadas que hemos creado no lo son. Así que haremos el análisis de componentes principales escalando las variables: Kilometers_Driven, Power, kmpl y kmpg.

pca2 <- prcomp(data[, c(3,4, 8, 9, 14, 15)], scale=T)

pca2
## Standard deviations (1, .., p=6):
## [1] 1.5099667 1.1444561 1.0599891 0.9273837 0.5500460 0.3522111
## 
## Rotation (n x k) = (6 x 6):
##                           PC1        PC2          PC3        PC4         PC5
## Year              -0.13713864 -0.3782024  0.596223411 0.63439205  0.28287926
## Kilometers_Driven  0.10115280  0.1465697 -0.668955170 0.71929106  0.04377908
## Engine             0.61114224 -0.2395341  0.007784461 0.01713324 -0.11602069
## Power              0.58101040 -0.3074207  0.085329519 0.04975366 -0.38891068
## kmpl              -0.50571711 -0.4201464 -0.135355634 0.08348424 -0.71719694
## kmpkg              0.06436916  0.7120907  0.413948973 0.26538370 -0.48885474
##                           PC6
## Year              -0.01412815
## Kilometers_Driven  0.03857703
## Engine            -0.74519368
## Power              0.63789605
## kmpl              -0.16752599
## kmpkg             -0.08956703

Aquí arriba, podemos ver los diferentes pesos que otorga el análisis de componentes principales a cada una de las variables iniciales escaladas. Por ejemplo: en la primera componente principal (PC1), vemos que sobre todo se enfrentan las variables Engine y Power contra kmpl; en la segunda componente principal (PC2), vemos que se enfrenta la variable kmpg contra kmpl, Power,Year y Engine; en la tercera componente principal (PC3) se enfrentan los kilómetros que lleva recorridos el coche contra el año de fabricación y la variable kmpkg.

summary(pca2)
## Importance of components:
##                         PC1    PC2    PC3    PC4     PC5     PC6
## Standard deviation     1.51 1.1445 1.0600 0.9274 0.55005 0.35221
## Proportion of Variance 0.38 0.2183 0.1873 0.1433 0.05043 0.02068
## Cumulative Proportion  0.38 0.5983 0.7856 0.9289 0.97932 1.00000

La inercia de las primeras dimensiones muestra si existen relaciones fuertes entre las variables y sugiere el número de dimensiones que se deben estudiar.

Las dos primeras dimensiones de análisis expresan el 59,83% de la inercia total del conjunto de datos; eso quiere decir que el 59.83% de los individuos (o variables) nublan la variabilidad total que es explicada por el plano. Este porcentaje es relativamente alto y, por lo tanto, el primer plano representa bien la variabilidad de los datos. Este valor es muy superior al valor de referencia que equivale al 34,95%, por lo que la variabilidad explicada por este plano es muy significativa (el valor de referencia es el cuantil 0,95 de la distribución de porcentajes de inercia obtenida simulando 1689 tablas de datos de tamaño equivalente sobre la base de una distribución normal).

A partir de estas observaciones, conviene interpretar también las dimensiones mayores o iguales a la tercera.

Sin embargo, aquí resulta difícil ver algo claro e intuitivo, así que haremos un pequeño resumen y un gráfico multivariante para mostrar la información más relevante del PCA.

Estudio del Plano 1:2

PC1= pca2[[2]][,1]
PC2= pca2[[2]][,2]
PC3= pca2[[2]][,3]
PC4= pca2[[2]][,4]

componentes_princ <- cbind(PC1,PC2,PC3,PC4)
componentes_princ
##                           PC1        PC2          PC3        PC4
## Year              -0.13713864 -0.3782024  0.596223411 0.63439205
## Kilometers_Driven  0.10115280  0.1465697 -0.668955170 0.71929106
## Engine             0.61114224 -0.2395341  0.007784461 0.01713324
## Power              0.58101040 -0.3074207  0.085329519 0.04975366
## kmpl              -0.50571711 -0.4201464 -0.135355634 0.08348424
## kmpkg              0.06436916  0.7120907  0.413948973 0.26538370
library(ade4)
plot(pca2)

biplot(pca2)

s.corcircle(componentes_princ[,-3], sub="PC1 Y PC2")

En el gráfico enfrentamos la primera y la segunda componente principal, y vemos como influyen cada una de las variables en los coches. Por ejemplo, el coche 1712, debe tener unos valores muy altos de Power y Engine que son las variables que “más tiran hacia la derecha”, y el coche 745, según el biplot debe tener un valor muy alto de kilometers_driven, que es la variable que “más tira en esa dirección”. Si recordamos lo analizado previamente, en la sección del análisis exploratorio de datos, este coche ya destacó por tener valores un tanto diferentes a los del resto por su desmesurado valor de kilómetros recorridos. Comprobamos que la información que nos proporciona el gráfico es totalmente coherente con lo que obtuvimos en el EDA.

La dimensión 1 opone individuos caracterizados por una coordenada fuertemente positiva en el eje (a la derecha del gráfico) a individuos caracterizados por una coordenada fuertemente negativa en el eje (a la izquierda del gráfico).

El grupo 1 (caracterizado por una coordenada positiva en el eje) comparte:

  • valores altos para la variable kmpkg.
  • valores bajos para las variables kmpl, Potencia y Motor (las variables se ordenan de la más débil).

El grupo 2 (caracterizado por una coordenada positiva en el eje) comparte:

  • valores altos para las variables Motor, Potencia y Kilómetros_Recorridos (las variables se ordenan de la más fuerte).
  • valores bajos para las variables kmpl, Year y kmpkg (las variables se ordenan de la más débil).

El grupo 3 (caracterizado por una coordenada negativa en el eje) comparte:

  • valores altos para las variables kmpl y Año (las variables se ordenan de la más fuerte).
  • valores bajos para las variables Motor, Potencia, Kilómetros_Conducidos y kmpkg (las variables se ordenan de menor a mayor).

La dimensión 2 opone individuos caracterizados por una coordenada fuertemente positiva en el eje (en la parte superior del gráfico) a individuos caracterizados por una coordenada fuertemente negativa en el eje (en la parte inferior del gráfico).

El grupo 1 (caracterizado por una coordenada positiva en el eje) comparte:

  • valores altos para las variables kmpl y Año (las variables se ordenan de la más fuerte).
  • valores bajos para las variables Motor, Potencia, Kilómetros_Conducidos y kmpkg (las variables se ordenan de menor a mayor).

El grupo 2 (caracterizado por una coordenada positiva en el eje) comparte:

  • valores altos para las variables Motor, Potencia y Kilómetros_Recorridos (las variables se ordenan de la más fuerte).
  • valores bajos para las variables kmpl, Year y kmpkg (las variables se ordenan de la más débil).

El grupo 3 (caracterizado por una coordenada negativa en el eje) comparte:

  • valores altos para la variable kmpkg.
  • valores bajos para las variables kmpl, Potencia y Motor (las variables se ordenan de la más débil).
library(factoextra)

fviz_pca_var(pca2,axes = c(1,2), col.var = "cos2", alpha.var = "contrib" ) + theme_grey()

fviz_pca_var(pca2,axes = c(1,3), col.var = "cos2", alpha.var = "contrib" ) + theme_grey()

La suma de cos2 de una variable determinada sobre cada factor es 1. Esto significa que cada vector debería estar tocando el perímetro de la circunferencia unidad, pero no lo está haciendo ninguna prácticamente, ¿por qué?. Si observamos por ejemplo la variable Engine(al igual que Power), vemos que está muy cerca de tocar dicho perímetro, su proyección sobre las dimensiones 1 y 2 (componentes) indica su contribución a éstas, pero aún le falta algo de contribución que debe estar repartida por otra u otras dimensiones. Si está variable solo tuviese peso sobre las dos primeras dimensiones estaría tocando la circunferencia.

Podemos colorear las observaciones según alguna variable. Además podemos hacer que las variables que más contribuyen en este plano factorial, se resalten más que las que menos influencia tienen. También tenemos la posibilidad de dibujar elipses alrededor de cada grupo con un cierto nivel de confianza.

Como se aprecia en los gráficos anteriores, no tiene mucho sentido representar la segunda componente principal ya que no realiza un correcto enfrentamiento de variables y no nos aporta nada.

#fviz_pca_ind(pca2,axes=c(1,2), col.ind = train$Gama, alpha.ind = "contrib" ) + theme_grey()
#fviz_pca_ind( pca2,axes=c(1,2),  habillage = as.factor( train$Gama ), addEllipses = TRUE, ellipse.level = 0.99 )
#fviz_pca_ind( pca2, axes=c(1,2),col.ind = train$Make, alpha.ind = "contrib" ) + theme_grey()
#fviz_pca_ind( pca2, axes=c(1,2),col.ind = train$Fuel_Type, alpha.ind = "contrib" ) + theme_grey()
#fviz_pca_ind( pca2, axes=c(1,2),habillage = as.factor( train$Fuel_Type ), addEllipses = TRUE, ellipse.level = 0.99 )
#fviz_pca_ind( pca2, axes=c(1,2),col.ind = train$Transmission, alpha.ind = "contrib" ) + theme_grey()
#fviz_pca_ind( pca2, axes=c(1,2),habillage = as.factor( train$Transmission ), addEllipses = TRUE, ellipse.level = 0.99 )
#fviz_pca_ind( pca2,axes=c(1,2), col.ind = train$Location, alpha.ind = "contrib" ) + theme_grey()
#fviz_pca_biplot( pca2, axes=c(1,2),col.ind = train$Gama )
summary(pca2)
## Importance of components:
##                         PC1    PC2    PC3    PC4     PC5     PC6
## Standard deviation     1.51 1.1445 1.0600 0.9274 0.55005 0.35221
## Proportion of Variance 0.38 0.2183 0.1873 0.1433 0.05043 0.02068
## Cumulative Proportion  0.38 0.5983 0.7856 0.9289 0.97932 1.00000

Por otro lado, podemos decir que lo que más nos interesa de este resumen es la proporción de la varianza total que consigue explicar cada componente principal. Según el resumen que acabamos de mostrar arriba, vemos que la varianza total explicada no aumenta mucho a partir de la tercera o cuarta componente principal (y que con todas las componentes principales, evidentemente, la varianza explicada es el 100%). Para visualizar esto haremos un gráfico de barras:

screeplot(pca2, xlab="PCs")

Una estimación del número correcto de ejes a interpretar sugiere restringir el análisis a la descripción de los 3 primeros ejes. Estos ejes presentan una inercia superior a las obtenidas por el cuantil 0,95 de las distribuciones aleatorias (78,56% frente a 51,79%). Esta observación sugiere que solo estos ejes llevan una información real. En consecuencia, la descripción se situará en estos ejes.

library(psych)
scree(data[, c(3,4, 8, 9, 14, 15)],main ="Grafico_de_Sedimentacion")

El grafico de sedimentación nos muestra la cantidad óptima de componentes a tomar en el análisis, siendo los valores por encima de la linea de 1.0 los más aceptables.

fa.parallel(data[, c(3,4, 8, 9, 14, 15)],fa="pc")

## Parallel analysis suggests that the number of factors =  NA  and the number of components =  3

Según los resultados del Análisis paralelo, el número de componentes deberá ser 3.

Se comprueba que con PCA no se consigue lo que se busca, ni PC2 ni PC3 nos sirven para realizar una correcta redimensión de los datos. Nos damos cuenta de que nuestro problema es bastante difícil de resolver, dado que es complicado ver algún tipo de separación o tendencia de los coches en función de las variables explicativas o incluso en cuanto al precio. Aún así, si nos fijamos, en los gráficos que enfrentan PC1 con PC2, PC1 con PC3 y PC2 con PC3, parece que los coches de gama media y gama alta están más dispersos que los de gama baja.

#library(FactoMineR)
#library(FactoInvestigate)
#res = PCA(train[, c(3,4, 8, 9, 14, 15)], graph=FALSE)
#Investigate(res) 

k-medias. Análisis cluster no jerárquico o de conglomerados (clustering)

El análisis cluster busca agrupar individuos u observaciones tratando de lograr la máxima homogeneidad en cada grupo o cluster y la mayor diferencia entre los grupos. Es decir, el clustering asigna individuos similares al mismo grupo, y observaciones muy distintas estarán en grupos diferentes. Nosotros usaremos el algoritmo de las k-medias que tiene como objetivo encontrar y agrupar en clases a las observaciones que tienen una alta similitud entre ellos. Esta similitud se define como la menor distancia entre características de cada observación. Cuanto más cerca estén los puntos de datos, más similares serán y más probabilidad habrá de que pertenezcan al mismo cluster. Para ello primero debemos escoger una distancia y dado que nuestra variable respuesta está bastante bien balanceada, usaremos la distancia euclídea. Esta es la distancia que muchos métodos de R utilizan por defecto, pero debemos asegurarnos de que los datos que introducimos en el algoritmo están escalados (para que no tengan mayor importancia las variables con números más grandes en valor absoluto, por el mero de hecho de que puedan estar medidas en diferentes unidades, por ejemplo).

TrainEscalado <- data %>% select(Year, Kilometers_Driven, Engine, Power, Price, kmpl) %>% scale() %>% as.data.frame()
#data[, c(3,4, 8, 9, 11,13)]
data <- data[,-13]
data %>% 
  mutate(
    Gama = case_when( 
      data$Make=="Datsun" |data$Make=="Smart" |data$Make=="Tata" |data$Make=="Fiat" |data$Make=="Chevrolet" |data$Make=="Ambassador" |
      data$Make=="Skoda"|data$Make=="Renault" |data$Make=="Ford" |data$Make=="Honda"|data$Make=="Volkswagen" |data$Make=="Hyundai" |data$Make=="Nissan" |data$Make=="Maruti" ~ "Baja-media",
      data$Make=="Bentley"|data$Make=="Porsche" |data$Make=="Land Rover" |data$Make=="Jaguar" |data$Make=="Mini" |data$Make=="Mercedes-Benz" |data$Make=="Audi" |data$Make=="Bmw" |data$Make=="Jeep" |data$Make=="Volvo" |data$Make=="Isuzu" |data$Make=="Mitsubishi" |data$Make=="Toyota" |data$Make=="Force" |data$Make=="Mahindra"~ "Alta"
    )
  ) -> data

data$Gama <- as.factor(data$Gama)

Como mínimo haremos dos grupos, es decir, buscaremos hacer 2 o más grupos, porque hacer un único cluster no tiene ningún sentido, ya que buscamos separar los coches en una característica que toma dos valores: gama baja-media y gama alta.

Para decidir el número óptimo de grupos que debemos crear, podemos usar la función NbClust de R, que nos devuelve cuál es (según unos criterios) el mejor número de clusters para el algoritmo de k-medias o bien, podemos ir probando con diferentes valores y decidir nosotros en función de la información que recabemos.

Primero usaremos la función, teniendo en cuenta que como máximo aceptaremos tener 10 grupos y como mínimo 2:

library(NbClust)
set.seed(220322)
#lo comento para que no se eejcute SIEMPRE pero es imprescincible , tarda mucho

#nc = NbClust(TrainEscalado,min.nc=2,max.nc=10,method = "kmeans")
#nc

La función NbClust prueba con diferente número de grupos y evalúa cuál es número óptimo de clusters según algunos criterios (muestra los resultados gráficos de la aplicación de algunos de ellos). Vemos que finalmente, nos dice que el número óptimo de grupos es 3, dado que es en el que más criterios de optimalidad coinciden.

Ahora, tras probar nosotros manualmente con diferente número de grupos, comprobamos que las mejores maneras para agrupar a los coches en función de la gama que tienen es creando 3,4 u 8 grupos.

# Ponemos una semilla para obtener siempre los mismos 3 grupos
set.seed(20112090)
km0 = kmeans(x=TrainEscalado, nstart = 5, centers = 3)
km0$centers #km2$[2]
##         Year Kilometers_Driven      Engine      Power      Price       kmpl
## 1  0.3253638        -0.0611996  1.37201471  1.4680576  1.4610785 -0.7413217
## 2 -1.0681538         0.2503815  0.04160836 -0.1492665 -0.4474343 -0.6201845
## 3  0.4455477        -0.1105182 -0.57602959 -0.5117040 -0.3478727  0.6340075
tkm0 <- table(km0$cluster,data$Gama)
tkm0
##    
##     Alta Baja-media
##   1  905         73
##   2  356        953
##   3   92       2332
# Ponemos una semilla para obtener siempre los mismos 4 grupos
set.seed(22032023)

km1 = kmeans(x=TrainEscalado, nstart = 5, centers = 4)
km1$centers #km1$[2]
##         Year Kilometers_Driven     Engine      Power      Price       kmpl
## 1 -1.2013643         0.1464077 -0.4491522 -0.5128053 -0.5978962 -0.3443545
## 2 -0.1286413         0.1717612  1.0387989  0.8212333  0.3662194 -0.7705951
## 3  0.5202489        -0.1219503 -0.5769438 -0.5043002 -0.3395386  0.6622250
## 4  0.6284743        -0.1958552  1.7519820  2.1655859  2.8047656 -0.7944150
tkm1 <- table(km1$cluster,data$Gama)
tkm1
##    
##     Alta Baja-media
##   1   77        953
##   2  870        232
##   3   74       2159
##   4  332         14
# Ponemos una semilla para obtener siempre los mismos 8 grupos
set.seed(20112020)
km2 = kmeans(x=TrainEscalado, nstart = 5, centers = 8)
km2$centers #km2$[2]
##         Year Kilometers_Driven     Engine      Power       Price       kmpl
## 1  0.7474046      -0.215013813 -0.4558985 -0.3368124 -0.23872151  0.1001606
## 2  0.6705904      -0.143829782  0.9138645  1.0616096  1.31598716 -0.4591534
## 3  0.6179466      -0.089033566 -0.7068906 -0.6775746 -0.38607087  1.3429620
## 4 -0.1313711       0.006899268 -0.7771851 -0.8573284 -0.47245641 -3.8415039
## 5 -0.5355855       0.062374606 -0.5472943 -0.5250797 -0.53531404  0.2572020
## 6 -2.1245518       0.281847224 -0.3699003 -0.5410684 -0.67361109 -0.3691450
## 7  0.2505949      -0.142263332  2.5613160  3.0441732  3.36430324 -1.0824906
## 8 -0.7006775       0.367237420  1.1640519  0.7542959 -0.01418397 -0.9321174
table(km2$cluster,data$Gama)
##    
##     Alta Baja-media
##   1   50        977
##   2  545         64
##   3   22        803
##   4    5         61
##   5   29        977
##   6   32        300
##   7  166          2
##   8  504        174
# Ponemos una semilla para obtener siempre los mismos 8 grupos
set.seed(201120572)
km3 = kmeans(x=TrainEscalado, nstart = 5, centers = 9)
km3$centers #km2$[2]
##         Year Kilometers_Driven     Engine      Power       Price        kmpl
## 1  0.7457212      -0.216085906 -0.4606091 -0.3420833 -0.24078272  0.08982149
## 2 -0.5380988       0.062578193 -0.5479289 -0.5259096 -0.53567271  0.25766538
## 3  0.2505949      -0.142263332  2.5613160  3.0441732  3.36430324 -1.08249057
## 4  0.6744025      -0.143545245  0.9136031  1.0614304  1.32163596 -0.45638120
## 5 -0.6978533       0.270519256  1.1603176  0.7529817 -0.01917599 -0.93074958
## 6 -0.1313711       0.006899268 -0.7771851 -0.8573284 -0.47245641 -3.84150392
## 7  1.1117978      63.738691629  2.2760090  2.7023168  4.98013063 -0.42798303
## 8  0.6197888      -0.088702794 -0.6984679 -0.6671467 -0.38233324  1.33768775
## 9 -2.1245518       0.281847224 -0.3699003 -0.5410684 -0.67361109 -0.36914499
table(km3$cluster,data$Gama)
##    
##     Alta Baja-media
##   1   50        968
##   2   29        973
##   3  166          2
##   4  542         63
##   5  506        176
##   6    5         61
##   7    1          0
##   8   22        815
##   9   32        300

Para no complicar demasiado el entendimiento del algoritmo, decidimos quedarnos con 3 clusters:

  • En el primer cluster, sobre todo, clasifica a los coches con valores altos de Price, Engine y Power. En esta categoría tenemos 978 coches, es decir, al 20.76% del total.

  • En el segundo cluster, si nos fijamos, clasifica a los coches con valores medios en casi todas las variables, con año de fabricación antiguo y kmpl alto. En esta categoría tenemos 1309 coches, es decir, al 27.79% del total.

  • En el tercer cluster, clasificamos a los coches con valores bajos en Precio, Engine y Power. En esta categoría tenemos 2424 coches relativamente comunes (sin valores atípicos o muy influyentes en ninguna de sus variables), es decir, al 51.45% del total.

Fijándonos en la tabla que nos devuelve, vemos que en el primer grupo la mayoría de los coches son de gama alta (el 92.54%); en el segundo grupo, los coches están bastante mezclados aunque son mayoría en la clase de gama media-baja pero necesitaríamos analizarlos más en profundidad para poder separarlos mejor (72.8% de coches de gama alta frente a un 27.2% de coches de gama media o baja); y en el tercer grupo, al contario que en el primero, la mayoría son de gama baja o media (un 96.2%)

Veamos esto que acabamos de explicar con algunos gráficos.

Según los cluster que hemos formado, si enfrentamos el precio (dominante en el grupo 1 con valores altos) frente a la variable Engine (dominante en el grupo 3 con valores bajos), deberíamos obtener un gráfico en el que los cluster 1 y 3 estuviesen bien diferenciados y el 2, que tenía valores medios, esté mezclado con ambos.

plot(TrainEscalado$Engine, TrainEscalado$Price, col=km0$cluster, pch=19 , cex=2, xlab = "Engine", ylab="Price", main = "Engine vs Price")
legend(-2,12,c("Cluster 1","Cluster 2","Cluster 3"),fill = (unique(sort(km0$cluster))))

Observamos como los coches de los cluster 1 y 3 están perfectamente separados. Además, esperábamos que el cluster 3 estuviese mezclado con los otros dos, sin embargo, vemos que enfrentando estas dos variables, separamos muy bien los tres grupos pese a que si que se juntan en ciertos coches.

Podríamos seguir haciendo gráficos y comprobaciones, pero con eso ya vemos que tenemos una buena forma de clasificar a algunos de los coches. En el grupo 1 teníamos mayoritariamente coches de gama alta; en el grupo 2, mezcla; y en el grupo 3, coches de gama baja o media. Hemos comprobado que los grupos 1 y 3 se separan muy bien gráficamente, pero de hecho, los grupos 1 y 2 también, dado que donde más “mezcla apreciamos es entre los cluster 2 y 3. Esto nos es de gran ayuda.

Clustering jerárquico

Para realizar el clustering jerárquico, también utilizaremos la distancia euclídea. Pero debemos definir cuál será la distancia entre dos grupos, que será la que nos sirva como criterio para decidir cuando se deben unir dos cluster. En R podemos definir diferentes distancias entre ellos (distancias entre los centroides de cada grupo, distancias entre los elementos más próximos de cada grupo, distancias entre los elementos más alejados de cada grupo…). Veremos cuál es el resultado de utilizar alguna de ellas mostrando los dendrogramas asociados a cada una.

En el primer caso utilizaremos el método single (distancia entre los elementos más cercanos de cada cluster):

hc1 = hclust(d=dist(TrainEscalado), method = "single" )
plot(hc1)

En este dendrograma, podemos ver que tenemos un coche atípico (el 745), que se acumula a la izquierda.

En el segundo caso utilizaremos el método complete (distancia entre los elementos más alejados de cada cluster):

hc2 = hclust(d=dist(TrainEscalado), method = "complete")
plot(hc2)

En este caso sucede algo parecido a lo que ocurría antes, el coche 745, vuelve a aparecer “al margen” del resto, y vuelve a destacar por ser uno de los últimos coches en agruparse en el dendrograma, aunque ya se empiezan a visualizar diferentes agrupaciones.

En el tercer caso utilizaremos el método average (distancia media entre las observaciones de cada cluster):

hc3 = hclust(d=dist(TrainEscalado), method = "average" )
plot(hc3)

En este tercer caso, de nuevo, volvemos a observar que el teorema 745 está más separado del resto que los demás entre sí, y que es este teorema el que provoca el “retraso” de la unión de las diferentes agrupaciones iniciales.

En el cuarto y último caso utilizaremos el método centroid (distancia entre los centroides de cada cluster):

hc4 = hclust(d=dist(TrainEscalado), method = "centroid" )
plot(hc4)

De nuevo, volvemos a identificar al teorema 745 a la izquierda, y volvemos a comprobar cómo es este teorema el que produce un mayor retraso en la última unión de todos los grupos.

Como ya se vio en el análisis exploratorio de datos, podríamos pensar en eliminarlo, pero como tampoco tenemos más información y no sabemos si en realidad son outliers o simplemente tienen características un poco diferentes a las de los demás, decidimos quedarnos con ellos, ya que puede que nos aparezca más adelante otro coche similar a alguno de ellos, y nos ayuden a clasificarlo adecuadamente.

Por último, tomando el segundo dendrograma, que parece ser en el que mejor se observan las diferentes agrupaciones, vamos a proceder a tomar únicamente 3 grupos (igual que hicimos en el clustering no jerárquico).

plot(hc2)
rect.hclust(hc2, k=4, border = "blue")

Con esto vemos que tenemos un cluster principal en el que se aglutinan la mayoría de los coches y otros dos formados por muy pocos coches (los más atípicos de los extremos del dendrograma). Veamos cómo al formar 10 grupos se observan mejor la subdivisiones.

plot(hc2)
rect.hclust(hc2, k=10, border = "blue")

Análisis supervisado

Regresión logística

La división de los coches en gama baja-media y gama alta ha sido necesaria también para poder realizar la regresión logística sobre la nueva variable.

library(dplyr)
#dataTest %>% select(-Mileage) -> dataTest
dataTest %>% select(-X) -> dataTest
head(dataTest)
dataTest%>%drop_na() -> dataTest
dataTest %>% 
    mutate(kmpl = ifelse(Fuel_Type=="Diesel" | Fuel_Type=="Petrol", Mileage, 0)) %>%
    mutate(kmpkg = ifelse(Fuel_Type=="CNG" | Fuel_Type=="LPG", Mileage, 0)) %>%
    select(-Mileage) -> dataTest

dataTest %>% 
  mutate(
    Gama = case_when( 
      dataTest$Make=="Datsun" |dataTest$Make=="Smart" |dataTest$Make=="Tata" |dataTest$Make=="Fiat" |dataTest$Make=="Chevrolet" |dataTest$Make=="Ambassador" |
      dataTest$Make=="Skoda"|dataTest$Make=="Renault" |dataTest$Make=="Ford" |dataTest$Make=="Honda"|dataTest$Make=="Volkswagen" |dataTest$Make=="Hyundai" |dataTest$Make=="Nissan" |dataTest$Make=="Maruti" ~ "Baja-media",
      dataTest$Make=="Bentley"|dataTest$Make=="Porsche" |dataTest$Make=="Land Rover" |dataTest$Make=="Jaguar" |dataTest$Make=="Mini" |dataTest$Make=="Mercedes-Benz" |dataTest$Make=="Audi" |dataTest$Make=="Bmw" |dataTest$Make=="Jeep" |dataTest$Make=="Volvo" |dataTest$Make=="Isuzu" |dataTest$Make=="Mitsubishi" |dataTest$Make=="Toyota" |dataTest$Make=="Force" |dataTest$Make=="Mahindra"~ "Alta"
    )
  ) -> dataTest

Se realizan las siguientes gráficas para visualizar los datos con la nueva variable:

table(data$Gama)
## 
##       Alta Baja-media 
##       1353       3358
ggplot(data, aes(x = Power, y = Price, color = Gama)) + geom_point()

ggplot(data, aes(x = Engine, y = Price, color = Gama)) + geom_point()

ggplot(data, aes(x = Transmission, y = Price, color = Gama)) + geom_point()

ggplot(data, aes(x = Seats, y = Price, color = Gama)) + geom_point()

ggplot(data, aes(x = Owner_Type, y = Price, color = Gama)) + geom_point()

Ahora, se van a aplicar los modelos con distintas covariables para buscar el mejor de ellos:

# modelos lineales generalizados estimados por MLE
logit <- glm( 
  Gama ~Power+Seats+Transmission+Owner_Type+Engine, 
  data = data, 
  family = binomial()
)
summary(logit)
## 
## Call:
## glm(formula = Gama ~ Power + Seats + Transmission + Owner_Type + 
##     Engine, family = binomial(), data = data)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.9207  -0.0737   0.1778   0.3066   4.0002  
## 
## Coefficients:
##                            Estimate Std. Error z value Pr(>|z|)    
## (Intercept)              -3.668e+00  4.878e+02  -0.008   0.9940    
## Power                    -1.940e-02  2.370e-03  -8.183 2.76e-16 ***
## Seats4                    1.186e+01  4.878e+02   0.024   0.9806    
## Seats5                    1.236e+01  4.878e+02   0.025   0.9798    
## Seats6                   -6.639e+00  6.166e+02  -0.011   0.9914    
## Seats7                    1.158e+01  4.878e+02   0.024   0.9811    
## Seats8                    9.311e+00  4.878e+02   0.019   0.9848    
## Seats9                    1.248e+01  4.878e+02   0.026   0.9796    
## Seats10                   1.171e+01  4.878e+02   0.024   0.9809    
## TransmissionManual        8.966e-01  1.476e-01   6.073 1.25e-09 ***
## Owner_TypeSecond         -6.252e-02  1.493e-01  -0.419   0.6755    
## Owner_TypeThird           6.359e-01  3.765e-01   1.689   0.0912 .  
## Owner_TypeFourth & Above  1.862e+00  1.113e+00   1.672   0.0945 .  
## Engine                   -3.224e-03  2.365e-04 -13.630  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 5649.7  on 4710  degrees of freedom
## Residual deviance: 2164.7  on 4697  degrees of freedom
## AIC: 2192.7
## 
## Number of Fisher Scoring iterations: 15
logit2 <- glm( 
  Gama ~Owner_Type+Seats+Transmission+Engine, 
  data = data, 
  family = binomial()
)
summary(logit2)
## 
## Call:
## glm(formula = Gama ~ Owner_Type + Seats + Transmission + Engine, 
##     family = binomial(), data = data)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.8689  -0.0929   0.1818   0.3542   4.0487  
## 
## Coefficients:
##                            Estimate Std. Error z value Pr(>|z|)    
## (Intercept)              -5.076e+00  4.895e+02  -0.010   0.9917    
## Owner_TypeSecond         -1.027e-02  1.475e-01  -0.070   0.9445    
## Owner_TypeThird           7.748e-01  3.781e-01   2.049   0.0405 *  
## Owner_TypeFourth & Above  2.289e+00  1.152e+00   1.987   0.0469 *  
## Seats4                    1.256e+01  4.895e+02   0.026   0.9795    
## Seats5                    1.327e+01  4.895e+02   0.027   0.9784    
## Seats6                   -5.571e+00  6.100e+02  -0.009   0.9927    
## Seats7                    1.297e+01  4.895e+02   0.026   0.9789    
## Seats8                    1.077e+01  4.895e+02   0.022   0.9824    
## Seats9                    1.508e+01  4.895e+02   0.031   0.9754    
## Seats10                   1.423e+01  4.895e+02   0.029   0.9768    
## TransmissionManual        1.361e+00  1.326e-01  10.269   <2e-16 ***
## Engine                   -4.555e-03  1.894e-04 -24.056   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 5649.7  on 4710  degrees of freedom
## Residual deviance: 2231.2  on 4698  degrees of freedom
## AIC: 2257.2
## 
## Number of Fisher Scoring iterations: 15
logit3 <- glm( 
  Gama ~Power+Transmission+Engine, 
  data = data, 
  family = binomial()
)
summary(logit3)
## 
## Call:
## glm(formula = Gama ~ Power + Transmission + Engine, family = binomial(), 
##     data = data)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.8679  -0.0698   0.1875   0.3241   4.2115  
## 
## Coefficients:
##                      Estimate Std. Error z value Pr(>|z|)    
## (Intercept)         9.3942879  0.3385829  27.746  < 2e-16 ***
## Power              -0.0140166  0.0021605  -6.488 8.73e-11 ***
## TransmissionManual  0.6373511  0.1373724   4.640 3.49e-06 ***
## Engine             -0.0040538  0.0001737 -23.345  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 5649.7  on 4710  degrees of freedom
## Residual deviance: 2298.2  on 4707  degrees of freedom
## AIC: 2306.2
## 
## Number of Fisher Scoring iterations: 6

La interpretación de los p-valores es similar a la del modelo lineal. Podemos ver que las variables Engine,Power y Transmission son significativas en el modelo (p-valor mucho menor de 0.05), mientras que la variable Seats y Owner_Type influyen más en un modelo que en otro.

El mejor modelo es el explicado por las variables Power, Engine y Transmission. En cuanto a los coeficientes, la interpretación cambia. El modelo GLM no ajusta la variable respuesta sino una función de enlace. En el caso del modelo logit esta función es: \(η=log(p1−p)\), siendo p la probabilidad de que el individuo tome el valor “1” correspondiente a la gama alta en la variable dicotómica. Al cociente \(p/(1−p)\) se le conoce como odds ratio. Por tanto, los coeficientes del modelo logit se interpretan como el logaritmo del odds ratio. Si nos fijamos en el coeficiente de la variable Transmission (0.63) en el modelo 3, nos está indicando que el logaritmo del odds ratio de pertenecer al grupo de los coches de alta gama aumenta 0.63 unidades por cada unidad que aumenta la variable Transmission.

Antes de comenzar con las siguientes, lo que debemos hacer es definir una medida de precisión para contrastar los datos una vez que tengamos cada matriz de confusión y comparar los resultados que nos ofrecen cada uno de los métodos que empleemos. En nuestro caso, la variable respuesta no está muy bien balanceada:

summary(data$Gama)
##       Alta Baja-media 
##       1353       3358

Como vemos, los coches de gama alta se corresponden con el 30% aproximadamente y los de gama baja-media, el 70%. Supondremos que están más o menos equilibradas. No queremos dar más valor a identificar un tipo de coche frente al otro. Por tanto, elegiremos la siguiente medida de precisión. Esta es una medida de precisión que hemos creado para tener en cuenta todos los casos, tanto los falsos negativos como los falsos positivos. En realidad, se trata de la media geométrica de la sensitividad (recall) y la especificidad, y se define según la siguiente expresión:

\[\text{Medida de precisión}=\sqrt{\frac{TP}{FN+TP}·\frac{TN}{FP+TN}}=\sqrt{TPR·TNR} \] donde:

  • \(TP\) (true positive) son los coches de gama alta que acertamos que son de gama alta.

  • \(TN\) (true negative) son los coches de gama baja-media que acertamos que son de baja-media.

  • \(FP\) (false positive) son los coches de gama baja-media que nosotros predecimos como gama alta.

  • \(FN\) (false negative) son los coches de gana alta que nosotros predecimos como de gama baja-media.

  • \(TPR\) (sensitivity, recall, hit rate or true positive rate) es la sensitividad.

  • \(TNR\) (specificity, selectivity or true negative rate) es la especificidad.

medidaPrecision <- function(matrizDeConfusion){
  return(sqrt(matrizDeConfusion[1,1]*matrizDeConfusion[2,2]/((matrizDeConfusion[1,2]+matrizDeConfusion[2,2])*(matrizDeConfusion[2,1]+matrizDeConfusion[1,1]))))
}

Los k-vecinos más cercanos (k-NN: The k-nearest neighbours)

El método de los k-vecinos más cercanos consiste en clasificar a un nuevo individuo en función de la categoría de sus \(k\) vecinos más cercanos, es decir, clasificaremos un coche en gama alta o gama media-baja en función de la gama de los teoremas más cercanos a él (con cercanía nos referimos a similitud entre sus características).

# Ponemos una semilla para que siempre nos salga el mismo resultado (el algoritmo es aleatorio)
set.seed(07122020)
library(caret)
library(ggplot2)
library(lattice)
library(class)
library(cluster)
library(lift)
trainX <- data[,c(2:10,13,14)]
preProcValues <- preProcess(x = trainX,method = c("center", "scale"))
preProcValues
## Created from 4711 samples and 11 variables
## 
## Pre-processing:
##   - centered (6)
##   - ignored (5)
##   - scaled (6)
set.seed(400)
ctrl <- trainControl(method="repeatedcv",repeats = 3) #,classProbs=TRUE,summaryFunction = twoClassSummary)
knnFit <- train(Gama ~ ., data = data[,c(2:10,13,14,15)], method = "knn", trControl = ctrl, preProcess = c("center","scale"), tuneLength = 20)

#Output of kNN fit
knnFit
## k-Nearest Neighbors 
## 
## 4711 samples
##   11 predictor
##    2 classes: 'Alta', 'Baja-media' 
## 
## Pre-processing: centered (30), scaled (30) 
## Resampling: Cross-Validated (10 fold, repeated 3 times) 
## Summary of sample sizes: 4240, 4240, 4240, 4240, 4240, 4240, ... 
## Resampling results across tuning parameters:
## 
##   k   Accuracy   Kappa    
##    5  0.9133979  0.7882405
##    7  0.9133945  0.7892998
##    9  0.9110582  0.7844472
##   11  0.9080157  0.7778724
##   13  0.9053970  0.7721609
##   15  0.9045491  0.7700798
##   17  0.9045492  0.7699856
##   19  0.9049044  0.7700120
##   21  0.9041972  0.7677520
##   23  0.9049056  0.7685929
##   25  0.9047639  0.7677360
##   27  0.9049062  0.7678410
##   29  0.9044819  0.7665081
##   31  0.9039856  0.7648794
##   33  0.9029251  0.7617441
##   35  0.9024993  0.7604944
##   37  0.9015801  0.7579416
##   39  0.9023573  0.7594855
##   41  0.9024273  0.7591610
##   43  0.9029213  0.7599820
## 
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was k = 5.
#Plotting yields Number of Neighbours Vs accuracy (based on repeated cross validation)
plot(knnFit)

knnPredict <- predict(knnFit,newdata =dataTest[,c(2:10,13,14,15)])
#knnPredict
data$Gama <- factor(data$Gama,levels=c("Alta","Baja-media"),labels=c("Alta","Baja-media"))
dataTest$Gama <- factor(dataTest$Gama,levels=c("Alta","Baja-media"),labels=c("Alta","Baja-media"))

#Get the confusion matrix to see accuracy value and other parameter values
confusionMatrix(knnPredict, dataTest$Gama )
## Confusion Matrix and Statistics
## 
##             Reference
## Prediction   Alta Baja-media
##   Alta        275         39
##   Baja-media   61        798
##                                           
##                Accuracy : 0.9147          
##                  95% CI : (0.8973, 0.9301)
##     No Information Rate : 0.7136          
##     P-Value [Acc > NIR] : < 2e-16         
##                                           
##                   Kappa : 0.7873          
##                                           
##  Mcnemar's Test P-Value : 0.03573         
##                                           
##             Sensitivity : 0.8185          
##             Specificity : 0.9534          
##          Pos Pred Value : 0.8758          
##          Neg Pred Value : 0.9290          
##              Prevalence : 0.2864          
##          Detection Rate : 0.2344          
##    Detection Prevalence : 0.2677          
##       Balanced Accuracy : 0.8859          
##                                           
##        'Positive' Class : Alta            
## 
accuracy <- mean(knnPredict == dataTest$Gama)
accuracy
## [1] 0.9147485
error = 1-accuracy
error
## [1] 0.08525149
table(knnPredict,dataTest$Gama)
##             
## knnPredict   Alta Baja-media
##   Alta        275         39
##   Baja-media   61        798

Por tanto, tomando el número óptimo de vecinos para realizar las predicciones, tenemos una precisión del 91’5%.

Árboles de decisión

Los árboles de decisión se construyen en base al cumplimiento o no de ciertos criterios en torno a las variables explicativas de los datos. Digamos que comenzamos comprobando un cierto criterio sobre la variable más explicativa. Si el criterio se cumple, nos vamos por una rama; si no, nos vamos por la otra. Por cada una de ellas, volveremos a comprobar otro cierto criterio, y así hasta llegar a las hojas. Las hojas clasifican a los datos en un grupo u otro. Todo esto lo explicaremos mucho mejor cuando construyamos nuestros árboles de decisión.

Primero veremos cuál es el valor óptimo de cp (parámetro de complejidad que combina la tasa de error con la profundidad del árbol). Para ello, construiremos un árbol muy complejo, y en función de los resultados que obtengamos lo “podaremos”.

# aquí no hace falta poner la semilla porque no es un algoritmo aleatorio
# set.seed(09122020)
#install.packages("rpart")
library(rpart)
#install.packages("rpart.plot")
library(rpart.plot)
#no metemos ni price ni make pq ambas son usadas para dividir la variable respuesta en dos gamas distintas
arbolDecisionInfoComplejo <- rpart(data[,15] ~., data[,c(2:10,13,14)], cp=0.001,  method="class",parms=list(split="information"))
plotcp(arbolDecisionInfoComplejo)

En el gráfico vemos que el mínimo se encuentra en torno al 0,0036, así que para obtener el mejor compromiso entre error y profundidad nos quedaremos con un valor de cp=0,004, porque tenemos que tener en cuenta que cuanto más pequeño sea este valor, más complejo y profundo será el árbol y menos error tendremos, sin embargo, corremos el riesgo de sobreajustar el modelo (problema de overfitting). Por otro lado, podemos usar dos criterios diferentes: el de información y el de Gini. Usaremos los dos y comprobaremos con cuál obtenemos una mayor precisión.

Comenzamos con el árbol de decisión utilizando el criterio de información.

arbolDecision <- rpart(data[,15] ~., data[,c(2:10,13,14)], cp=0.004,  method="class",parms=list(split="information"))
arbolDecision
## n= 4711 
## 
## node), split, n, loss, yval, (yprob)
##       * denotes terminal node
## 
##   1) root 4711 1353 Baja-media (0.287200170 0.712799830)  
##     2) Engine>=1690 1548  291 Alta (0.812015504 0.187984496)  
##       4) Power>=164.2 860   65 Alta (0.924418605 0.075581395)  
##         8) Fuel_Type=Diesel 726   31 Alta (0.957300275 0.042699725)  
##          16) Seats=4,5,6,8 533    7 Alta (0.986866792 0.013133208) *
##          17) Seats=7 193   24 Alta (0.875647668 0.124352332)  
##            34) Engine>=2299.5 177   10 Alta (0.943502825 0.056497175)  
##              68) kmpl>=10.955 166    0 Alta (1.000000000 0.000000000) *
##              69) kmpl< 10.955 11    1 Baja-media (0.090909091 0.909090909) *
##            35) Engine< 2299.5 16    2 Baja-media (0.125000000 0.875000000) *
##         9) Fuel_Type=Petrol 134   34 Alta (0.746268657 0.253731343)  
##          18) Power>=180.515 96    9 Alta (0.906250000 0.093750000) *
##          19) Power< 180.515 38   13 Baja-media (0.342105263 0.657894737)  
##            38) Power< 177.235 7    0 Alta (1.000000000 0.000000000) *
##            39) Power>=177.235 31    6 Baja-media (0.193548387 0.806451613) *
##       5) Power< 164.2 688  226 Alta (0.671511628 0.328488372)  
##        10) Engine>=2117.5 420   68 Alta (0.838095238 0.161904762)  
##          20) Power< 140.5 335   22 Alta (0.934328358 0.065671642)  
##            40) Power< 137 229    6 Alta (0.973799127 0.026200873) *
##            41) Power>=137 106   16 Alta (0.849056604 0.150943396)  
##              82) Power>=138.555 90    0 Alta (1.000000000 0.000000000) *
##              83) Power< 138.555 16    0 Baja-media (0.000000000 1.000000000) *
##          21) Power>=140.5 85   39 Baja-media (0.458823529 0.541176471)  
##            42) kmpl>=13.59 31    6 Alta (0.806451613 0.193548387) *
##            43) kmpl< 13.59 54   14 Baja-media (0.259259259 0.740740741)  
##              86) Engine< 2188.5 9    0 Alta (1.000000000 0.000000000) *
##              87) Engine>=2188.5 45    5 Baja-media (0.111111111 0.888888889) *
##        11) Engine< 2117.5 268  110 Baja-media (0.410447761 0.589552239)  
##          22) Engine< 1796.5 22    0 Alta (1.000000000 0.000000000) *
##          23) Engine>=1796.5 246   88 Baja-media (0.357723577 0.642276423)  
##            46) Power< 155.875 210   88 Baja-media (0.419047619 0.580952381)  
##              92) Engine< 1798.5 28    1 Alta (0.964285714 0.035714286) *
##              93) Engine>=1798.5 182   61 Baja-media (0.335164835 0.664835165)  
##               186) Engine>=1932 134   61 Baja-media (0.455223881 0.544776119)  
##                 372) kmpl< 16.88 76   26 Alta (0.657894737 0.342105263)  
##                   744) kmpl>=15.05 39    2 Alta (0.948717949 0.051282051) *
##                   745) kmpl< 15.05 37   13 Baja-media (0.351351351 0.648648649) *
##                 373) kmpl>=16.88 58   11 Baja-media (0.189655172 0.810344828)  
##                   746) kmpl>=20.19 8    0 Alta (1.000000000 0.000000000) *
##                   747) kmpl< 20.19 50    3 Baja-media (0.060000000 0.940000000) *
##               187) Engine< 1932 48    0 Baja-media (0.000000000 1.000000000) *
##            47) Power>=155.875 36    0 Baja-media (0.000000000 1.000000000) *
##     3) Engine< 1690 3163   96 Baja-media (0.030350933 0.969649067)  
##       6) Engine>=1353.5 1250   80 Baja-media (0.064000000 0.936000000)  
##        12) Engine< 1366 30    0 Alta (1.000000000 0.000000000) *
##        13) Engine>=1366 1220   50 Baja-media (0.040983607 0.959016393)  
##          26) Seats=4,7 59   19 Baja-media (0.322033898 0.677966102)  
##            52) Power>=99.41 31   13 Alta (0.580645161 0.419354839)  
##             104) Fuel_Type=Diesel 15    0 Alta (1.000000000 0.000000000) *
##             105) Fuel_Type=Petrol 16    3 Baja-media (0.187500000 0.812500000) *
##            53) Power< 99.41 28    1 Baja-media (0.035714286 0.964285714) *
##          27) Seats=5,8 1161   31 Baja-media (0.026701120 0.973298880)  
##            54) Engine< 1496.5 416   27 Baja-media (0.064903846 0.935096154)  
##             108) Engine>=1495.5 18    0 Alta (1.000000000 0.000000000) *
##             109) Engine< 1495.5 398    9 Baja-media (0.022613065 0.977386935) *
##            55) Engine>=1496.5 745    4 Baja-media (0.005369128 0.994630872) *
##       7) Engine< 1353.5 1913   16 Baja-media (0.008363826 0.991636174)  
##        14) Seats=6 7    0 Alta (1.000000000 0.000000000) *
##        15) Seats=4,5,7,8 1906    9 Baja-media (0.004721931 0.995278069) *

Aquí tenemos nuestro árbol de decisión. Como vemos, la primera pregunta (2 y 3) que le hará a un nuevo coche es si su motor es mayor o igual o menor a 1690. Si es mayor o igual a 1690, le preguntará si la potencia es inferior a 164.2 (preguntas 4 y 5); y si es que no, preguntará si el tipo de combustible es de diésel o petróleo(preg 8 y 9). Si es de diésel, entonces habrá que fijarse en el número de asientos que posea el coche. En este punto del árbol, si el número de asientos es 7, se volverá a clasificar por el motor, en función de si es mayor a 2299 en el que cerraremos la rama del árbol observando si la variable kmpl es inferior a 10. Así se ha llegado a un nodo hoja (marcados en asterisco) como por ejemplo llegar hasta la condición 69 y cumplirla, donde se clasifica el coche en gama media-baja o gama alta (dependiendo de la hoja en cuestión a la que haya llegado el coche). Como acabamos de señalar, las variables más informativas de nuestro árbol son el motor y la potencia de los coches. Tiene sentido que esto sea así, y que la primera característica sobre la que “pregunte” el árbol para clasificar a un nuevo coche sea Engine. ¿Por qué? Porque tal y como vimos, en la sección del análisis exploratorio de datos, justo era ésta la covariable más relacionada con gama de los coches. Así que los resultados obtenidos son perfectamente coherentes con todo el estudio realizado hasta el momento.

Aquí tenemos las preguntas que se han hecho a lo largo del árbol.

labels(arbolDecision, pretty=T)
##  [1] "root"           "Engine>=1690"   "Power>=164.2"   "Fuel_Type=Disl"
##  [5] "Seats=4,5,6,8"  "Seats=7"        "Engine>=2300"   "kmpl>=10.96"   
##  [9] "kmpl< 10.96"    "Engine< 2300"   "Fuel_Type=Ptrl" "Power>=180.5"  
## [13] "Power< 180.5"   "Power< 177.2"   "Power>=177.2"   "Power< 164.2"  
## [17] "Engine>=2118"   "Power< 140.5"   "Power< 137"     "Power>=137"    
## [21] "Power>=138.6"   "Power< 138.6"   "Power>=140.5"   "kmpl>=13.59"   
## [25] "kmpl< 13.59"    "Engine< 2188"   "Engine>=2188"   "Engine< 2118"  
## [29] "Engine< 1796"   "Engine>=1796"   "Power< 155.9"   "Engine< 1798"  
## [33] "Engine>=1798"   "Engine>=1932"   "kmpl< 16.88"    "kmpl>=15.05"   
## [37] "kmpl< 15.05"    "kmpl>=16.88"    "kmpl>=20.19"    "kmpl< 20.19"   
## [41] "Engine< 1932"   "Power>=155.9"   "Engine< 1690"   "Engine>=1354"  
## [45] "Engine< 1366"   "Engine>=1366"   "Seats=4,7"      "Power>=99.41"  
## [49] "Fuel_Type=Disl" "Fuel_Type=Ptrl" "Power< 99.41"   "Seats=5,8"     
## [53] "Engine< 1496"   "Engine>=1496"   "Engine< 1496"   "Engine>=1496"  
## [57] "Engine< 1354"   "Seats=6"        "Seats=4,5,7,8"

Los errores que se cometen en cada hoja y el error total del árbol:

printcp(arbolDecision)
## 
## Classification tree:
## rpart(formula = data[, 15] ~ ., data = data[, c(2:10, 13, 14)], 
##     method = "class", parms = list(split = "information"), cp = 0.004)
## 
## Variables actually used in tree construction:
## [1] Engine    Fuel_Type kmpl      Power     Seats    
## 
## Root node error: 1353/4711 = 0.2872
## 
## n= 4711 
## 
##           CP nsplit rel error  xerror      xstd
## 1  0.7139690      0  1.000000 1.00000 0.0229528
## 2  0.0177384      1  0.286031 0.28603 0.0139298
## 3  0.0162602      3  0.250554 0.27494 0.0136808
## 4  0.0110865      4  0.234294 0.24908 0.0130737
## 5  0.0096083      6  0.212121 0.21951 0.0123293
## 6  0.0088692     10  0.173688 0.19956 0.0117915
## 7  0.0081301     12  0.155950 0.19586 0.0116884
## 8  0.0066519     13  0.147820 0.18551 0.0113933
## 9  0.0059128     14  0.141168 0.17221 0.0109993
## 10 0.0051737     17  0.123429 0.16482 0.0107727
## 11 0.0048780     18  0.118256 0.15743 0.0105401
## 12 0.0044346     23  0.093865 0.14043 0.0099802
## 13 0.0040000     29  0.064302 0.12712 0.0095146
# Para pintar un árbol
# Mejor mirar el esquema anterior
rpart.plot(arbolDecision, uniform=T);

# Otra forma para pintarlo:
#library(rattle)
#fancyRpartPlot(arbolDecision)

Vamos a comprobar la precisión del árbol con los mismos datos con los que ha sido construido.

predArbolInfo1 = predict(arbolDecision, data[,c(2:10,13,14)])
# Redondeamos la predicción para obtener un resultado 0-1
predArbolInfo1 = round(predArbolInfo1)
tArb1 = table(predArbolInfo1[,1], data[,15])
tArb2 = table(predArbolInfo1[,2], data[,15])
#medidaPrecision(tArb1)
medidaPrecision(tArb2)
## [1] 0.9745568

Obtenemos una precisión del 97%, lo que esperamos que disminuya al utilizar nuevos datos. Vamos a ver como clasifica nuestro árbol a nuevos teoremas. Para ello usaremos el conjunto de datos de testing y calcularemos la medida de precisión del resultado.

predArbolInfo2 = predict(arbolDecision,dataTest[,c(2:10,14,15)])
# Redondeamos la predicción para obtener un resultado 0-1
predArbolInfo2 = round(predArbolInfo2)
tArb3 = table(predArbolInfo2[,1], dataTest[,13])
tArb4 = table(predArbolInfo2[,2], dataTest[,13])
#medidaPrecision(tArb3)
medidaPrecision(tArb4)
## [1] 0.9553525

Efectivamente vemos como la precisión del 95% es menor, aunque tampoco ha disminuido mucho, lo que nos indica que es un buen árbol y que no hemos sobreajustado el modelo.

Por último, vamos a ver cuál es la precisión que obtenemos utilizando el criterio de Ginni.

Construimos un nuevo árbol:

arbolDecisionGini <- rpart(data[,15] ~., data=data[,c(2:10,13,14)], cp=0.004, parms=list(split="gini"))
arbolDecisionGini
## n= 4711 
## 
## node), split, n, loss, yval, (yprob)
##       * denotes terminal node
## 
##   1) root 4711 1353 Baja-media (0.287200170 0.712799830)  
##     2) Engine>=1690 1548  291 Alta (0.812015504 0.187984496)  
##       4) Power>=164.2 860   65 Alta (0.924418605 0.075581395)  
##         8) Fuel_Type=Diesel 726   31 Alta (0.957300275 0.042699725) *
##         9) Fuel_Type=Petrol 134   34 Alta (0.746268657 0.253731343)  
##          18) Power>=180.515 96    9 Alta (0.906250000 0.093750000) *
##          19) Power< 180.515 38   13 Baja-media (0.342105263 0.657894737)  
##            38) Power< 177.235 7    0 Alta (1.000000000 0.000000000) *
##            39) Power>=177.235 31    6 Baja-media (0.193548387 0.806451613) *
##       5) Power< 164.2 688  226 Alta (0.671511628 0.328488372)  
##        10) Engine>=2117.5 420   68 Alta (0.838095238 0.161904762)  
##          20) Power< 151 379   37 Alta (0.902374670 0.097625330)  
##            40) Fuel_Type=Diesel 371   31 Alta (0.916442049 0.083557951)  
##              80) Engine< 2498.5 332   17 Alta (0.948795181 0.051204819)  
##               160) Power< 137 198    0 Alta (1.000000000 0.000000000) *
##               161) Power>=137 134   17 Alta (0.873134328 0.126865672)  
##                 322) Power>=138.555 118    1 Alta (0.991525424 0.008474576) *
##                 323) Power< 138.555 16    0 Baja-media (0.000000000 1.000000000) *
##              81) Engine>=2498.5 39   14 Alta (0.641025641 0.358974359)  
##               162) Engine>=2511 27    3 Alta (0.888888889 0.111111111) *
##               163) Engine< 2511 12    1 Baja-media (0.083333333 0.916666667) *
##            41) Fuel_Type=Petrol 8    2 Baja-media (0.250000000 0.750000000) *
##          21) Power>=151 41   10 Baja-media (0.243902439 0.756097561) *
##        11) Engine< 2117.5 268  110 Baja-media (0.410447761 0.589552239)  
##          22) Engine< 1796.5 22    0 Alta (1.000000000 0.000000000) *
##          23) Engine>=1796.5 246   88 Baja-media (0.357723577 0.642276423)  
##            46) Kilometers_Driven< 41183.5 43   11 Alta (0.744186047 0.255813953) *
##            47) Kilometers_Driven>=41183.5 203   56 Baja-media (0.275862069 0.724137931)  
##              94) kmpl< 16.755 137   52 Baja-media (0.379562044 0.620437956)  
##               188) kmpl>=14.09 72   26 Alta (0.638888889 0.361111111)  
##                 376) Power>=134.1 53   10 Alta (0.811320755 0.188679245)  
##                   752) Power< 147.555 45    2 Alta (0.955555556 0.044444444) *
##                   753) Power>=147.555 8    0 Baja-media (0.000000000 1.000000000) *
##                 377) Power< 134.1 19    3 Baja-media (0.157894737 0.842105263) *
##               189) kmpl< 14.09 65    6 Baja-media (0.092307692 0.907692308) *
##              95) kmpl>=16.755 66    4 Baja-media (0.060606061 0.939393939) *
##     3) Engine< 1690 3163   96 Baja-media (0.030350933 0.969649067)  
##       6) Seats=6 7    0 Alta (1.000000000 0.000000000) *
##       7) Seats=4,5,7,8 3156   89 Baja-media (0.028200253 0.971799747)  
##        14) Power>=132.16 7    2 Alta (0.714285714 0.285714286) *
##        15) Power< 132.16 3149   84 Baja-media (0.026675135 0.973324865)  
##          30) Engine>=1353.5 1243   75 Baja-media (0.060337892 0.939662108)  
##            60) Engine< 1366 29    0 Alta (1.000000000 0.000000000) *
##            61) Engine>=1366 1214   46 Baja-media (0.037891269 0.962108731) *
##          31) Engine< 1353.5 1906    9 Baja-media (0.004721931 0.995278069) *

Que utiliza las siguientes condiciones acerca de las características de los coches con los errores siguientes:

labels(arbolDecisionGini, pretty=T)
##  [1] "root"                         "Engine>=1690"                
##  [3] "Power>=164.2"                 "Fuel_Type=Disl"              
##  [5] "Fuel_Type=Ptrl"               "Power>=180.5"                
##  [7] "Power< 180.5"                 "Power< 177.2"                
##  [9] "Power>=177.2"                 "Power< 164.2"                
## [11] "Engine>=2118"                 "Power< 151"                  
## [13] "Fuel_Type=Disl"               "Engine< 2498"                
## [15] "Power< 137"                   "Power>=137"                  
## [17] "Power>=138.6"                 "Power< 138.6"                
## [19] "Engine>=2498"                 "Engine>=2511"                
## [21] "Engine< 2511"                 "Fuel_Type=Ptrl"              
## [23] "Power>=151"                   "Engine< 2118"                
## [25] "Engine< 1796"                 "Engine>=1796"                
## [27] "Kilometers_Driven< 4.118e+04" "Kilometers_Driven>=4.118e+04"
## [29] "kmpl< 16.76"                  "kmpl>=14.09"                 
## [31] "Power>=134.1"                 "Power< 147.6"                
## [33] "Power>=147.6"                 "Power< 134.1"                
## [35] "kmpl< 14.09"                  "kmpl>=16.76"                 
## [37] "Engine< 1690"                 "Seats=6"                     
## [39] "Seats=4,5,7,8"                "Power>=132.2"                
## [41] "Power< 132.2"                 "Engine>=1354"                
## [43] "Engine< 1366"                 "Engine>=1366"                
## [45] "Engine< 1354"
printcp(arbolDecisionGini)
## 
## Classification tree:
## rpart(formula = data[, 15] ~ ., data = data[, c(2:10, 13, 14)], 
##     parms = list(split = "gini"), cp = 0.004)
## 
## Variables actually used in tree construction:
## [1] Engine            Fuel_Type         Kilometers_Driven kmpl             
## [5] Power             Seats            
## 
## Root node error: 1353/4711 = 0.2872
## 
## n= 4711 
## 
##          CP nsplit rel error  xerror      xstd
## 1 0.7139690      0   1.00000 1.00000 0.0229528
## 2 0.0177384      1   0.28603 0.29268 0.0140761
## 3 0.0162602      3   0.25055 0.27273 0.0136302
## 4 0.0155211      4   0.23429 0.27273 0.0136302
## 5 0.0073910      6   0.20325 0.22764 0.0125399
## 6 0.0072062      9   0.17886 0.17591 0.0111105
## 7 0.0059128     13   0.15004 0.16408 0.0107497
## 8 0.0044346     14   0.14412 0.15004 0.0103011
## 9 0.0040000     22   0.10791 0.13378 0.0097507
# Para pintar el árbol
rpart.plot(arbolDecisionGini, uniform=T);

#text(arbolDecisionGini, all=T, pretty=0, fancy=T, use.n=T, fwidth=0.3, fheight=0.3)
# Otra forma para pintarlo
#fancyRpartPlot(arbolDecisionGini)

Así que en en nuestro conjunto de entrenamiento obtenemos la siguiente precisión:

predArbolGini1 = predict(arbolDecisionGini,data[,c(2:10,13,14)])
# Redondeamos la predicción para obtener un resultado 0-1
predArbolGini1 = round(predArbolGini1)
tArb5 = table(predArbolGini1[,1], data[,15])
tArb6 = table(predArbolGini1[,2], data[,15])
#medidaPrecision(tArb5)
medidaPrecision(tArb6)
## [1] 0.9587796

Y en con el conjunto de prueba obtenemos una precisión:

predArbolGini2 = predict(arbolDecisionGini, dataTest[,c(2:10,14,15)])
predArbolGini2=round(predArbolGini2)
tArb7 = table(predArbolGini2[,1],dataTest[,13])
tArb8 = table(predArbolGini2[,2],dataTest[,13])
pArb1 = medidaPrecision(tArb7)
pArb2 = medidaPrecision(tArb8)
#pArb1
pArb2
## [1] 0.9329822

Comprobamos que tanto con el conjunto de entrenamiento como con el conjunto de prueba las precisiones que obtenemos son muy buenas también con Gini, en este caso se obtiene un 95% en entrenamiento y un 93% en test por lo que consideramos buenos árboles de decisión.

Bosques aleatorios

Un bosque de árboles o bosque aleatorio es un conjunto de árboles de decisión tales que los nodos de cada árbol dependen de los valores de un subconjunto de variables muestreado aleatoriamente. Es decir, para construir cada árbol se escogen un subconjunto del total de las variables explicativas. Cuando tenemos el bosque construido lo que hacemos es “pasar” los nuevos datos por todos los árboles del bosque. Cada árbol clasificará al dato en una clase, y el bosque, finalmente, clasificará al dato en la clase en la que más árboles hayan coincidido.

Una vez que sabemos qué es un bosque aleatorio cabe preguntarse cuál es el número óptimo de árboles que debemos construir y cuántas variables queremos que se seleccionen para cada uno de los árboles. Para descubrir esto, probaremos con distintos valores de cada uno y nos quedaremos con el que mejor medida de precisión nos proporcione.

library(ranger)
set.seed(123)

modelo <- ranger(
  formula = as.factor(data[,15]) ~ .,
  data = data[,c(2:10,13,14)],
  num.trees = 10,
  seed = 123
)
predicciones <- predict(modelo, data = dataTest[,c(2:10,14,15)])
predicciones
## Ranger prediction
## 
## Type:                             Classification 
## Sample size:                      1173 
## Number of independent variables:  11
test_rmse <- sqrt(mean((predicciones - as.factor(data[,13]))^2))
paste("Error de test (rmse) del modelo (log): ", round(test_rmse,2))
## [1] "Error de test (rmse) del modelo (log):  NA"

Máquinas de vectores de soporte (SVM: Support Vector Machines)

Vamos a aplicar SVM a nuestros datos. Las SVM constituyen un método basado en aprendizaje para la resolución de problemas de clasificación y regresión. En ambos casos, esta resolución se basa en una primera fase de entrenamiento (donde se les informa con múltiples ejemplos ya resueltos, en forma de pares {problema, solución}) y una segunda fase de uso para la resolución de problemas. En ella, las SVM se convierten en una “caja negra” que proporciona una respuesta (salida) a un problema dado (entrada).

library(e1071)
svmdata = data[,c(2:10,13,14,15)]

Lo primero que hacemos es obtener los conjuntos de train y test de los datos que vamos a usar.

# Creamos el conjunto de Train y Test

ind <- sample(2,nrow(svmdata), replace= TRUE, prob = c(0.7,0.3))
trainSet <- svmdata[ind==2,]
testSet <- svmdata[ind==1,]

Realizamos el entrenamiento del modelo. Vamosa probar los distintos kernels.

SVM con kernel radial

# Entrenamos el modelo
model <- svm(Gama~., data = trainSet, kernel="radial")
prediccion <- predict(model, newdata= testSet[-12])
# Resultado y porcentaje de acierto
#plot(model, data = svmdata, Bwt~Hwt)
MC <- table(testSet[,12], prediccion) #matriz de confusión
MC
##             prediccion
##              Alta Baja-media
##   Alta        845         96
##   Baja-media  139       2222
accuracy <- (sum(diag(MC)))/(sum(MC))
accuracy
## [1] 0.928831

SVM con kernel lineal

# Resultado y porcentaje de acierto
#plot(model, data = svmdata, Bwt~Hwt)
model2 <- svm(Gama~., data = trainSet, kernel="linear")
prediccion <- predict(model2, newdata= testSet[-12])

MC <- table(testSet[,12], prediccion) #matriz de confusión
MC
##             prediccion
##              Alta Baja-media
##   Alta        848         93
##   Baja-media  140       2221
accuracy <- (sum(diag(MC)))/(sum(MC))
accuracy
## [1] 0.9294367

SVM con kernel sigmoidal

# Resultado y porcentaje de acierto
#plot(model, data = svmdata, Bwt~Hwt)
model3 <- svm(Gama~., data = trainSet, kernel="sigmoid")
prediccion <- predict(model3, newdata= testSet[-12])

MC <- table(testSet[,12], prediccion) #matriz de confusión
MC
##             prediccion
##              Alta Baja-media
##   Alta        848         93
##   Baja-media  142       2219
accuracy <- (sum(diag(MC)))/(sum(MC))
accuracy
## [1] 0.928831

SVM con kernel polinomial

# Resultado y porcentaje de acierto
#plot(model, data = svmdata, Bwt~Hwt)
model4 <- svm(Gama~., data = trainSet, kernel="polynomial")
prediccion <- predict(model4, newdata= testSet[-12])

MC <- table(testSet[,12], prediccion) #matriz de confusión
MC
##             prediccion
##              Alta Baja-media
##   Alta        322        619
##   Baja-media   26       2335
accuracy <- (sum(diag(MC)))/(sum(MC))
accuracy
## [1] 0.8046638

Aún que los resultados son parecidos en 3 de los 4 kernels, hay uno que sobresale y es el kernel “Radial”. Por lo tanto es el mejor hiperplano que se adapta a nuestros datos.

Ajuste de los hiperparámetros del modelo

Regresión logística

El método glm de caret emplea la función glm() del paquete básico de R. Este algoritmo no tiene ningún hiperparámetro pero, para que efectúe una regresión logística, hay que indicar family = “binomial”.

library(doMC)
registerDoMC(cores = 4)
# HIPERPARÁMETROS, NÚMERO DE REPETICIONES Y SEMILLAS PARA CADA REPETICIÓN
particiones  <- 10
repeticiones <- 5
# Hiperparámetros
hiperparametros <- data.frame(parameter = "none")

set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
  seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# DEFINICIÓN DEL ENTRENAMIENTO
control_train <- trainControl(method = "repeatedcv", number = particiones,
                              repeats = repeticiones, seeds = seeds,
                              returnResamp = "final", verboseIter = FALSE,
                              allowParallel = TRUE)
# AJUSTE DEL MODELO
set.seed(34220)
modelo_logistic <- train(Gama ~ ., data = data[,c(2:10,13,14,15)],
                         method = "glm",
                         tuneGrid = hiperparametros,
                         metric = "Accuracy",
                         trControl = control_train,
                         family = "binomial")
modelo_logistic
## Generalized Linear Model 
## 
## 4711 samples
##   11 predictor
##    2 classes: 'Alta', 'Baja-media' 
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold, repeated 5 times) 
## Summary of sample sizes: 4240, 4239, 4240, 4240, 4239, 4241, ... 
## Resampling results:
## 
##   Accuracy   Kappa    
##   0.9266427  0.8214096

Empleando un modelo de regresión logística se consigue un accuracy promedio de validación del 93%.

k-NN

Ahora aplicamos la busqueda bayesiana para encontrar el mejor número de vecinos para el kNN.

library(foreach)
library(iterators)
library(parallel)
# Hiperparámetros
hiperparametros <- data.frame(k = c(1, 2, 5, 10, 15, 20, 30, 50))

set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
  seeds[[i]] <- sample.int(1000, nrow(hiperparametros)) 
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# DEFINICIÓN DEL ENTRENAMIENTO
library(caret)
library(ggplot2)
library(lattice)
control_train <- trainControl(method = "repeatedcv", number = particiones,
                              repeats = repeticiones, seeds = seeds,
                              returnResamp = "final", verboseIter = FALSE,
                              allowParallel = TRUE)
# AJUSTE DEL MODELO

set.seed(34220)
modelo_knn <- train(Gama~ ., data = data[,c(2:10,13,14,15)],
                    method = "knn",
                    tuneGrid = hiperparametros,
                    metric = "Accuracy",
                    trControl = control_train)
modelo_knn
## k-Nearest Neighbors 
## 
## 4711 samples
##   11 predictor
##    2 classes: 'Alta', 'Baja-media' 
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold, repeated 5 times) 
## Summary of sample sizes: 4240, 4239, 4240, 4240, 4239, 4241, ... 
## Resampling results across tuning parameters:
## 
##   k   Accuracy   Kappa    
##    1  0.8794749  0.7056852
##    2  0.8680960  0.6771780
##    5  0.8976852  0.7480572
##   10  0.8866888  0.7157664
##   15  0.8693664  0.6631850
##   20  0.8526423  0.6066279
##   30  0.8244122  0.5050079
##   50  0.7812349  0.3321058
## 
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was k = 5.
# REPRESENTACIÓN GRÁFICA
ggplot(modelo_knn, highlight = TRUE) +
  scale_x_continuous(breaks = hiperparametros$k) +
  labs(title = "Evolución del accuracy del modelo KNN", x = "K") +
  theme_bw()

Con un modelo kNN con k=5 se consigue un accuracy de validación promedio del 89’7%.

Árboles de decisión

# Hiperparámetros
hiperparametros <- data.frame(parameter = "none")

set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
  seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# DEFINICIÓN DEL ENTRENAMIENTO
control_train <- trainControl(method = "repeatedcv", number = particiones,
                              repeats = repeticiones, seeds = seeds,
                              returnResamp = "final", verboseIter = FALSE,
                              allowParallel = TRUE)
# AJUSTE DEL MODELO
set.seed(34220)
modelo_C50Tree <- train(Gama~ ., data = data[,c(2:10,13,14,15)],
                    method = "C5.0Tree",
                    tuneGrid = hiperparametros,
                    metric = "Accuracy",
                    trControl = control_train)
modelo_C50Tree
## Single C5.0 Tree 
## 
## 4711 samples
##   11 predictor
##    2 classes: 'Alta', 'Baja-media' 
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold, repeated 5 times) 
## Summary of sample sizes: 4240, 4239, 4240, 4240, 4239, 4241, ... 
## Resampling results:
## 
##   Accuracy  Kappa    
##   0.981405  0.9544161
summary(modelo_C50Tree$finalModel)
## 
## Call:
## C50:::C5.0.default(x = x, y = y, weights = wts)
## 
## 
## C5.0 [Release 2.07 GPL Edition]      Fri Apr  1 17:48:53 2022
## -------------------------------
## 
## Class specified by attribute `outcome'
## 
## Read 4711 cases (31 attributes) from undefined.data
## 
## Decision tree:
## 
## Engine <= 1599:
## :...Seats5 <= 0:
## :   :...Power <= 99:
## :   :   :...Seats6 <= 0: Baja-media (128/1)
## :   :   :   Seats6 > 0: Alta (7)
## :   :   Power > 99:
## :   :   :...Fuel_TypeDiesel <= 0:
## :   :       :...Engine <= 1499: Baja-media (13)
## :   :       :   Engine > 1499: Alta (3)
## :   :       Fuel_TypeDiesel > 0:
## :   :       :...Seats8 <= 0: Alta (15)
## :   :           Seats8 > 0: Baja-media (2)
## :   Seats5 > 0:
## :   :...Engine <= 1343:
## :       :...kmpl > 18.33: Baja-media (1346/1)
## :       :   kmpl <= 18.33:
## :       :   :...kmpl <= 17.6: Baja-media (344)
## :       :       kmpl > 17.6:
## :       :       :...Power <= 77: Baja-media (60)
## :       :           Power > 77:
## :       :           :...Power <= 82: Alta (8)
## :       :               Power > 82: Baja-media (50)
## :       Engine > 1343:
## :       :...Engine <= 1364: Alta (30)
## :           Engine > 1364:
## :           :...Engine > 1496: Baja-media (745/4)
## :               Engine <= 1496:
## :               :...Engine > 1493:
## :                   :...Engine <= 1495: Baja-media (13)
## :                   :   Engine > 1495: Alta (18)
## :                   Engine <= 1493:
## :                   :...Fuel_TypeDiesel <= 0: Baja-media (77/5)
## :                       Fuel_TypeDiesel > 0:
## :                       :...Power > 66: Baja-media (276)
## :                           Power <= 66:
## :                           :...Power <= 64: Baja-media (24)
## :                               Power > 64: Alta (4)
## Engine > 1599:
## :...Power > 163.7:
##     :...Fuel_TypeDiesel <= 0:
##     :   :...Power > 180: Alta (96/9)
##     :   :   Power <= 180:
##     :   :   :...Power <= 177.01: Alta (7)
##     :   :       Power > 177.01:
##     :   :       :...Engine <= 1797: Alta (5)
##     :   :           Engine > 1797: Baja-media (26/1)
##     :   Fuel_TypeDiesel > 0:
##     :   :...kmpl <= 10.93:
##     :       :...kmpl <= 10.6: Alta (25)
##     :       :   kmpl > 10.6: Baja-media (10)
##     :       kmpl > 10.93:
##     :       :...Engine > 2200: Alta (301)
##     :           Engine <= 2200:
##     :           :...Engine > 2149:
##     :               :...Engine <= 2179: Alta (26)
##     :               :   Engine > 2179: Baja-media (14)
##     :               Engine <= 2149:
##     :               :...Power > 167.7: Alta (326/3)
##     :                   Power <= 167.7:
##     :                   :...Power <= 167.62: Alta (20)
##     :                       Power > 167.62: Baja-media (4)
##     Power <= 163.7:
##     :...Seats8 > 0: Alta (95)
##         Seats8 <= 0:
##         :...Power > 147.8:
##             :...kmpl > 18.7: Alta (7)
##             :   kmpl <= 18.7:
##             :   :...kmpl > 12.9:
##             :       :...Fuel_TypeDiesel <= 0: Baja-media (29)
##             :       :   Fuel_TypeDiesel > 0:
##             :       :   :...Engine <= 1984: Alta (3)
##             :       :       Engine > 1984: Baja-media (26/2)
##             :       kmpl <= 12.9:
##             :       :...Engine <= 2092:
##             :           :...TransmissionManual <= 0: Alta (15/3)
##             :           :   TransmissionManual > 0: Baja-media (2)
##             :           Engine > 2092:
##             :           :...Engine <= 2400: Baja-media (17)
##             :               Engine > 2400:
##             :               :...Engine <= 2773: Alta (6/1)
##             :                   Engine > 2773: Baja-media (6)
##             Power <= 147.8:
##             :...Engine <= 1997:
##                 :...Engine <= 1798: Alta (45/1)
##                 :   Engine > 1798:
##                 :   :...Power <= 139.01:
##                 :       :...Seats5 > 0: Baja-media (81/1)
##                 :       :   Seats5 <= 0:
##                 :       :   :...Year <= 2014: Baja-media (3)
##                 :       :       Year > 2014: Alta (3)
##                 :       Power > 139.01:
##                 :       :...kmpl <= 14.8: Baja-media (10)
##                 :           kmpl > 14.8:
##                 :           :...kmpl <= 16.8: Alta (32)
##                 :               kmpl > 16.8:
##                 :               :...Power <= 142: Baja-media (13)
##                 :                   Power > 142: Alta (6)
##                 Engine > 1997:
##                 :...Year <= 2011:
##                     :...Power <= 120.7: Alta (31/1)
##                     :   Power > 120.7:
##                     :   :...kmpl <= 14.49: Baja-media (23/1)
##                     :       kmpl > 14.49: Alta (4)
##                     Year > 2011:
##                     :...Power > 138.1: Alta (106)
##                         Power <= 138.1:
##                         :...Power > 136: Baja-media (9)
##                             Power <= 136:
##                             :...Power > 88.73: Alta (103/1)
##                                 Power <= 88.73:
##                                 :...Power <= 70.02: Alta (9)
##                                     Power > 70.02: Baja-media (4)
## 
## 
## Evaluation on training data (4711 cases):
## 
##      Decision Tree   
##    ----------------  
##    Size      Errors  
## 
##      57   35( 0.7%)   <<
## 
## 
##     (a)   (b)    <-classified as
##    ----  ----
##    1337    16    (a): class Alta
##      19  3339    (b): class Baja-media
## 
## 
##  Attribute usage:
## 
##  100.00% Engine
##   68.99% Seats5
##   58.01% kmpl
##   45.38% Power
##   28.27% Fuel_TypeDiesel
##   14.96% Seats8
##    6.26% Year
##    2.87% Seats6
##    0.36% TransmissionManual
## 
## 
## Time: 0.0 secs

Empleando como modelo un árbol simple C5.0, se consigue un accuracy promedio de validación del 98’1%.

Random Forest

El método ranger de caret emplea la función ranger() del paquete ranger. Este algoritmo tiene 3 hiperparámetros:

  • mtry: número predictores seleccionados aleatoriamente en cada árbol.

  • min.node.size: tamaño mínimo que tiene que tener un nodo para poder ser dividido.

  • splitrule: criterio de división.

Aunque caret también incluye el método rf con la función rf() del paquete randomForest, este último solo permite optimizar el hiperparámetro mtry.

# Hiperparámetros
hiperparametros <- expand.grid(mtry = c(3, 4, 5, 7),
                               min.node.size = c(2, 3, 4, 5, 10, 15, 20, 30),
                               splitrule = "gini")

set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
  seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# DEFINICIÓN DEL ENTRENAMIENTO
control_train <- trainControl(method = "repeatedcv", number = particiones,
                              repeats = repeticiones, seeds = seeds,
                              returnResamp = "final", verboseIter = FALSE,
                              allowParallel = TRUE)
# AJUSTE DEL MODELO

set.seed(34220)
modelo_rf <- train(Gama ~ ., data = data[,c(2:10,13,14,15)],
                   method = "ranger",
                   tuneGrid = hiperparametros,
                   metric = "Accuracy",
                   trControl = control_train,
                   # Número de árboles ajustados
                   num.trees = 500)
modelo_rf
## Random Forest 
## 
## 4711 samples
##   11 predictor
##    2 classes: 'Alta', 'Baja-media' 
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold, repeated 5 times) 
## Summary of sample sizes: 4240, 4239, 4240, 4240, 4239, 4241, ... 
## Resampling results across tuning parameters:
## 
##   mtry  min.node.size  Accuracy   Kappa    
##   3      2             0.9507990  0.8806760
##   3      3             0.9509681  0.8810356
##   3      4             0.9509680  0.8810744
##   3      5             0.9509255  0.8810132
##   3     10             0.9499067  0.8785655
##   3     15             0.9486324  0.8754411
##   3     20             0.9471894  0.8719629
##   3     30             0.9449389  0.8663402
##   4      2             0.9646382  0.9138751
##   4      3             0.9637462  0.9117117
##   4      4             0.9634485  0.9109872
##   4      5             0.9633642  0.9108188
##   4     10             0.9611564  0.9054930
##   4     15             0.9588218  0.8998535
##   4     20             0.9565711  0.8945093
##   4     30             0.9516469  0.8825840
##   5      2             0.9750379  0.9390685
##   5      3             0.9743592  0.9374681
##   5      4             0.9741901  0.9370737
##   5      5             0.9736372  0.9357505
##   5     10             0.9719390  0.9316412
##   5     15             0.9698595  0.9266553
##   5     20             0.9668035  0.9192999
##   5     30             0.9612837  0.9060455
##   7      2             0.9835284  0.9597806
##   7      3             0.9829770  0.9584159
##   7      4             0.9830614  0.9586304
##   7      5             0.9826792  0.9577242
##   7     10             0.9816599  0.9552617
##   7     15             0.9796647  0.9504233
##   7     20             0.9780094  0.9464033
##   7     30             0.9733400  0.9349875
## 
## Tuning parameter 'splitrule' was held constant at a value of gini
## Accuracy was used to select the optimal model using the largest value.
## The final values used for the model were mtry = 7, splitrule = gini
##  and min.node.size = 2.
modelo_rf$finalModel
## Ranger result
## 
## Call:
##  ranger::ranger(dependent.variable.name = ".outcome", data = x,      mtry = min(param$mtry, ncol(x)), min.node.size = param$min.node.size,      splitrule = as.character(param$splitrule), write.forest = TRUE,      probability = classProbs, ...) 
## 
## Type:                             Classification 
## Number of trees:                  500 
## Sample size:                      4711 
## Number of independent variables:  30 
## Mtry:                             7 
## Target node size:                 2 
## Variable importance mode:         none 
## Splitrule:                        gini 
## OOB prediction error:             1.59 %
# REPRESENTACIÓN GRÁFICA
ggplot(modelo_rf, highlight = TRUE) +
  scale_x_continuous(breaks = 1:30) +
  labs(title = "Evolución del accuracy del modelo Random Forest") +
  guides(color = guide_legend(title = "mtry"),
         shape = guide_legend(title = "mtry")) +
  theme_bw()

Empleando un modelo random forest con mtry = 7, min.node.size = 2 y splitrule = “gini”, se consigue un accuracy promedio de validación del 98’4%.

SVM

El método svmRadial de caret emplea la función ksvm() del paquete kernlab. Este algoritmo tiene 2 hiperparámetros:

  • sigma: coeficiente del kernel radial.

  • C: penalización por violaciones del margen del hiperplano.

# Hiperparámetros
hiperparametros <- expand.grid(sigma = c(0.001, 0.01, 0.1, 0.5, 1),
                               C = c(1 , 20, 50, 100, 200, 500, 700))

set.seed(123)
seeds <- vector(mode = "list", length = (particiones * repeticiones) + 1)
for (i in 1:(particiones * repeticiones)) {
  seeds[[i]] <- sample.int(1000, nrow(hiperparametros))
}
seeds[[(particiones * repeticiones) + 1]] <- sample.int(1000, 1)
# DEFINICIÓN DEL ENTRENAMIENTO
control_train <- trainControl(method = "repeatedcv", number = particiones,
                              repeats = repeticiones, seeds = seeds,
                              returnResamp = "final", verboseIter = FALSE,
                              allowParallel = TRUE)
# AJUSTE DEL MODELO

set.seed(342)
modelo_svmrad <- train(Gama ~ ., data = data[,c(2:10,13,14,15)],
                   method = "svmRadial",
                   tuneGrid = hiperparametros,
                   metric = "Accuracy",
                   trControl = control_train)
modelo_svmrad
## Support Vector Machines with Radial Basis Function Kernel 
## 
## 4711 samples
##   11 predictor
##    2 classes: 'Alta', 'Baja-media' 
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold, repeated 5 times) 
## Summary of sample sizes: 4240, 4240, 4239, 4240, 4240, 4240, ... 
## Resampling results across tuning parameters:
## 
##   sigma  C    Accuracy   Kappa    
##   0.001    1  0.9271074  0.8244331
##   0.001   20  0.9269789  0.8236833
##   0.001   50  0.9277433  0.8254514
##   0.001  100  0.9279981  0.8257346
##   0.001  200  0.9292287  0.8283934
##   0.001  500  0.9296964  0.8290602
##   0.001  700  0.9297806  0.8289960
##   0.010    1  0.9296963  0.8298518
##   0.010   20  0.9300766  0.8287067
##   0.010   50  0.9293980  0.8270251
##   0.010  100  0.9294410  0.8270782
##   0.010  200  0.9284219  0.8245447
##   0.010  500  0.9269788  0.8206876
##   0.010  700  0.9271481  0.8208105
##   0.100    1  0.9260447  0.8192948
##   0.100   20  0.9201869  0.8035070
##   0.100   50  0.9173425  0.7958958
##   0.100  100  0.9155593  0.7916444
##   0.100  200  0.9123322  0.7840549
##   0.100  500  0.9063028  0.7695067
##   0.100  700  0.9036705  0.7629402
##   0.500    1  0.9010397  0.7500010
##   0.500   20  0.8968771  0.7400260
##   0.500   50  0.8900000  0.7235634
##   0.500  100  0.8852446  0.7115387
##   0.500  200  0.8838005  0.7079148
##   0.500  500  0.8812541  0.7010558
##   0.500  700  0.8806599  0.6992638
##   1.000    1  0.8944579  0.7267584
##   1.000   20  0.8871119  0.7111117
##   1.000   50  0.8814231  0.6968666
##   1.000  100  0.8800224  0.6931900
##   1.000  200  0.8783665  0.6888309
##   1.000  500  0.8773057  0.6860360
##   1.000  700  0.8773906  0.6861693
## 
## Accuracy was used to select the optimal model using the largest value.
## The final values used for the model were sigma = 0.01 and C = 20.
modelo_svmrad$finalModel
## Support Vector Machine object of class "ksvm" 
## 
## SV type: C-svc  (classification) 
##  parameter : cost C = 20 
## 
## Gaussian Radial Basis kernel function. 
##  Hyperparameter : sigma =  0.01 
## 
## Number of Support Vectors : 878 
## 
## Objective Function Value : -13539.15 
## Training error : 0.057737
# REPRESENTACIÓN GRÁFICA
ggplot(modelo_svmrad, highlight = TRUE) +
  labs(title = "Evolución del accuracy del modelo SVM Radial") +
  theme_bw()

Empleando un modelo SVM Radial con sigma = 0.01 y C = 20, se consigue un accuracy promedio de validación del 93%.

Evaluación y comparación de modelos

Para poder determinar si un método es superior a otro, no es suficiente con comparar los mínimos (o máximos dependiendo de la métrica) que ha conseguido cada uno, sino que hay que tener en cuenta sus varianzas para determinar si existen evidencias suficientes de superioridad.

Al tratarse de modelos entrenados y validados sobre los mismos datos, mismas particiones y en el mismo orden (siempre que se haya asegurado la reproducibilidad mediante semillas), se pueden emplear métodos estadísticos para datos dependientes.

modelos <- list(KNN = modelo_knn, logistic = modelo_logistic,
                arbol = modelo_C50Tree, rf = modelo_rf,
                SVMradial = modelo_svmrad)

resultados_resamples <- resamples(modelos)
# Se trasforma el dataframe devuelto por resamples() para separar el nombre del
# modelo y las métricas en columnas distintas.
metricas_resamples <- resultados_resamples$values %>%
                         gather(key = "modelo", value = "valor", -Resample) %>%
                         separate(col = "modelo", into = c("modelo", "metrica"),
                                  sep = "~", remove = TRUE)
metricas_resamples %>% 
  group_by(modelo, metrica) %>% 
  summarise(media = mean(valor)) %>%
  spread(key = metrica, value = media) %>%
  arrange(desc(Accuracy))
metricas_resamples %>%
  filter(metrica == "Accuracy") %>%
  group_by(modelo) %>% 
  summarise(media = mean(valor)) %>%
  ggplot(aes(x = reorder(modelo, media), y = media, label = round(media, 2))) +
    geom_segment(aes(x = reorder(modelo, media), y = 0,
                     xend = modelo, yend = media),
                     color = "grey50") +
    geom_point(size = 7, color = "firebrick") +
    geom_text(color = "white", size = 2.5) +
    scale_y_continuous(limits = c(0, 1)) +
    # Accuracy basal
    geom_hline(yintercept = 0.88, linetype = "dashed") +
    labs(title = "Validación: Accuracy medio repeated-CV",
         subtitle = "Modelos ordenados por media",
         x = "modelo") +
    coord_flip() +
    theme_bw()

metricas_resamples %>% filter(metrica == "Accuracy") %>%
  group_by(modelo) %>% 
  mutate(media = mean(valor)) %>%
  ungroup() %>%
  ggplot(aes(x = reorder(modelo, media), y = valor, color = modelo)) +
    geom_boxplot(alpha = 0.6, outlier.shape = NA) +
    geom_jitter(width = 0.1, alpha = 0.6) +
    scale_y_continuous(limits = c(0, 1)) +
    # Accuracy basal
    geom_hline(yintercept = 0.88, linetype = "dashed") +
    theme_bw() +
    labs(title = "Validación: Accuracy medio repeated-CV",
         subtitle = "Modelos ordenados por media") +
    coord_flip() +
    theme(legend.position = "none")

El modelo random forest consigue el accuracy promedio más alto, seguido muy de cerca por el árbol de decisión y SVM. Para determinar si las diferencias entre ellos son significativas, se recurre a test estadísticos.

Test de Friedman para comparar el accuracy de los modelos

matriz_metricas <- metricas_resamples %>% filter(metrica == "Accuracy") %>%
                   spread(key = modelo, value = valor) %>%
                   select(-Resample, -metrica) %>% as.matrix()
friedman.test(y = matriz_metricas)
## 
##  Friedman rank sum test
## 
## data:  matriz_metricas
## Friedman chi-squared = 177.79, df = 4, p-value < 2.2e-16

Para un nivel de significancia (α = 0.05), el test de Friedman sí encuentra evidencias para rechazar la hipótesis nula de que los clasificadores consiguen la misma precisión, sin embargo, no determina que par o pares son diferentes. Para identificarlos, se recurre a contrastes post HOC.

La función diff() del paquete caret recibe como argumento los resultados de validación de dos o más modelos extraídos con resample() y hace comparaciones por pares aplicando un t-test pareado con correcciones por comparaciones múltiples. Esta función no permite mucha flexibilidad en cuanto a las comparaciones, por lo que, una vez extraídos los datos con resample(), suele ser preferible emplear otras funciones disponibles en R.

# Comparaciones múltiples con un test suma de rangos de Wilcoxon
metricas_accuracy <- metricas_resamples %>% filter(metrica == "Accuracy")
comparaciones  <- pairwise.wilcox.test(x = metricas_accuracy$valor, 
                                        g = metricas_accuracy$modelo,
                                        paired = TRUE,
                                        p.adjust.method = "holm")

# Se almacenan los p_values en forma de dataframe
comparaciones <- comparaciones$p.value %>%
  as.data.frame() %>%
  rownames_to_column(var = "modeloA") %>%
  gather(key = "modeloB", value = "p_value", -modeloA) %>%
  na.omit() %>%
  arrange(modeloA) 

comparaciones

Acorde a las comparaciones por pares, no existen evidencias suficientes para considerar que la capacidad predictiva de los modelos es distinta.

Curva de ROC

Las curvas ROC (Receiver Operating Characteritic curve) permiten evaluar, en problemas de clasificación binaria, cómo varia la proporción de verdaderos positivos (sensibilidad o recall) y la de falsos positivos (1-especificidad) dependiendo del cutoff de probabilidad empleado en las asignaciones. El gráfico resultante es muy útil para identificar el cutoff que consigue un mejor equilibrio sensibilidad-especificidad. Además de esto, la curva ROC, en concreto el área bajo la curva (AUC), puede emplearse como métrica para evaluar modelos. Un modelo que clasifica perfectamente las dos clases tiene un 100% de sensibilidad y especificidad, por lo que el área bajo la curva es de 1. Un modelo que predice por debajo de lo esperado por azar, tiene un AUC menor de 0.5. Una condición necesaria para crear una curva ROC es disponer de la probabilidad de clases en las predicciones.

En caret, se puede sustituir la métrica accuracy empleada por defecto en problemas de clasificación y calcular en su lugar el AUC. Para ello, se tienen que indicar los argumentos summaryFunction = twoClassSummary y classProbs = TRUE en el control de entrenamiento. El segundo argumento es necesario porque el cálculo de la curva ROC requiere las probabilidades predichas para cada clase. Además del área bajo la curva, se calcula la sensibilidad y la especificidad para un cutoff de 0.5.

El paquete pROC contiene múltiples funciones para crear, representar y obtener métricas a partir de curvas ROC. Como argumentos se necesitan únicamente las probabilidades predichas para cada clase y la clase verdadera a la que pertenece cada observación.

library(pROC)
# Se obtienen las probabilidades predichas para cada clase
predicciones <- predict(object = modelo_logistic,
                        newdata = dataTest[,c(2:10,13,14,15)],
                        type = "prob")
# Cálculo de la curva
curva_roc <- roc(response = dataTest$Gama, 
                 predictor = predicciones$Alta) 

# Gráfico de la curva
plot(curva_roc)

# Área bajo la curva AUC
auc(curva_roc)
## Area under the curve: 0.9471
# Intervalo de confianza de la curva
ci.auc(curva_roc, conf.level = 0.95)
## 95% CI: 0.9314-0.9629 (DeLong)
library(pROC)
# Se obtienen las probabilidades predichas para cada clase
predicciones <- predict(object = modelo_knn,
                        newdata = dataTest[,c(2:10,13,14,15)],
                        type = "prob")
# Cálculo de la curva
curva_roc <- roc(response = dataTest$Gama, 
                 predictor = predicciones$Alta) 

# Gráfico de la curva
plot(curva_roc)

# Área bajo la curva AUC
auc(curva_roc)
## Area under the curve: 0.9184
# Intervalo de confianza de la curva
ci.auc(curva_roc, conf.level = 0.95)
## 95% CI: 0.8995-0.9372 (DeLong)
# library(pROC)
# # Se obtienen las probabilidades predichas para cada clase
# predicciones <- predict(object = modelo_rf,
#                         newdata = dataTest[,c(2:10,13,14,15)],
#                         type = "prob")
# # Cálculo de la curva
# curva_roc <- roc(response = dataTest$Gama, 
#                  predictor = predicciones$Alta) 
# 
# # Gráfico de la curva
# plot(curva_roc)
# Área bajo la curva AUC
auc(curva_roc)
## Area under the curve: 0.9184
# Intervalo de confianza de la curva
ci.auc(curva_roc, conf.level = 0.95)
## 95% CI: 0.8995-0.9372 (DeLong)
library(pROC)
# Se obtienen las probabilidades predichas para cada clase
predicciones <- predict(object = modelo_C50Tree,
                        newdata = dataTest[,c(2:10,13,14,15)],
                        type = "prob")
# Cálculo de la curva
curva_roc <- roc(response = dataTest$Gama, 
                 predictor = predicciones$Alta) 

# Gráfico de la curva
plot(curva_roc)

# Área bajo la curva AUC
auc(curva_roc)
## Area under the curve: 0.9939
# Intervalo de confianza de la curva
ci.auc(curva_roc, conf.level = 0.95)
## 95% CI: 0.9898-0.998 (DeLong)

Redes bayesianas y/o GAM